/* holddown.c -- Holddown timer
 *
 *    Copyright 2010 Patrick Allen
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
*/

#include <inttypes.h>
#include <time.h>
#include <stdlib.h>
#include <net/if.h>
#include <signal.h>

#include "advrp.h"
#include "holddown.h"
#include "debug.h"

void * holddown_counter(void * notused);
void holddown_del(hdNetwork * net);

extern uint32_t HOLDDOWN_TIMER;
extern pthread_mutex_t mutexAdvrp;
extern pthread_cond_t condCreate;
extern pthread_mutex_t mutexCreate;

pthread_mutex_t mutexHolddown, mutexHaveHolddown;
pthread_cond_t condHolddown;
_Bool haveHolddown;
hdNetwork * firstHdNetwork = NULL, * lastHdNetwork = NULL;

/* on_holddown
 * Check if this prefix is on holddown.
 */
_Bool on_holddown(uint64_t hash)
{
	hdNetwork * net;

	for (net = firstHdNetwork; net != NULL; net = net->next)
		if (net->hash == hash)
			return 1;
	return 0;
}

/* holddown_add
 * Place this prefix on holddown.
 * XXX: When a prefix is added to holddown, remove any other route to that 
 * prefix installed by another neighbor?
 */
void holddown_add(uint64_t hash)
{
	hdNetwork * new = NULL;

	DBG(DEV_INOUT, "Entered function.");
	pthread_mutex_lock(&mutexHolddown);

	if (on_holddown(hash)) {
		DBG(RECV_1, "Prefix %llu is already on holddown.", hash);
		pthread_mutex_unlock(&mutexHolddown);
		return;
	}

	if (! (new = malloc(sizeof(hdNetwork)))) {
		DBG(ERR, "Malloc failed. Exiting.");
		cleanup();
	}

	DBG(RECV_1, "Placing route with hash %llu on holddown.", hash);
	new->hash = hash;
	new->timer = time(NULL);

	if (lastHdNetwork) {
		DBG(DEV_2, "Holddown network is not the first.");
		lastHdNetwork->next = new;
		new->prev = lastHdNetwork;
		lastHdNetwork = new;
	} else {
		DBG(HOLD_2, "Assigning new first holddown network.");
		firstHdNetwork = lastHdNetwork = new;
		new->prev = NULL;
		pthread_mutex_lock(&mutexHaveHolddown);
		haveHolddown = 1;
		pthread_mutex_unlock(&mutexHaveHolddown);
		pthread_cond_signal(&condHolddown);
	}

	pthread_mutex_unlock(&mutexHolddown);
	DBG(DEV_INOUT, "Leaving function.");
	return;
}

/* holddown_counter
 * Wait for a route to be placed on holddown.  Remove routes from holddown when
 * their timer has expired.
 */
void * holddown_loop(void * notused)
{
	time_t sleeptimer = HOLDDOWN_TIMER;
	sigset_t newmask;

	/* Drop effecitve privs */
	drop_privs(1);

	/* Ignore signals */
	newmask.__val[0] = SIGINT;
	newmask.__val[1] = SIGTERM;
	newmask.__val[2] = SIGHUP;
	pthread_sigmask(SIG_SETMASK, &newmask, 0);

	DBG(DEV_1, "Holddown thread id: %u", pthread_self());

	/* Tell main to continue creating threads */
	pthread_mutex_lock(&mutexCreate);
	pthread_cond_signal(&condCreate);
	pthread_mutex_unlock(&mutexCreate);

	while (1) {
		pthread_mutex_lock(&mutexHaveHolddown);
		if (! haveHolddown) {
			/* Don't have any routes on holddown, wait */
			DBG(HOLD_1, "No prefixes on holddown, waiting...");
			pthread_mutex_unlock(&mutexHaveHolddown);
			pthread_cond_wait(&condHolddown, &mutexAdvrp);
			pthread_mutex_unlock(&mutexAdvrp);
		} else {
			pthread_mutex_unlock(&mutexHaveHolddown);
		}

		/* Have at least one route on holddown, check to see if any
		 * should come off of holddown */
		DBG(HOLD_1, "Holddown network found.");
		pthread_mutex_lock(&mutexAdvrp);
		sleeptimer = check_holddowns();
		pthread_mutex_unlock(&mutexAdvrp);
		DBG(HOLD_2, "Sleeping for %u seconds.", (uint32_t)sleeptimer);
		sleep(sleeptimer);
	}
}

/* check_holddowns
 * Check all networks on holddown, and remove any with an expired timer.
 * Return the time that the next check should occur at.
 */
time_t check_holddowns(void)
{
	hdNetwork * curr;
	time_t nextchk = HOLDDOWN_TIMER + 1;
	time_t ctime = time(NULL);            /* current time */
	time_t age;         /* how long ago a route was put on holddown */

	DBG(DEV_INOUT, "Entered function.");

	for (curr = firstHdNetwork; curr != NULL; curr = curr->next) {
		if ((age = ctime - curr->timer) > HOLDDOWN_TIMER) {
			/* route can come off of holddown */
			holddown_del(curr);
		} else if (HOLDDOWN_TIMER - age < nextchk) {
			nextchk = (HOLDDOWN_TIMER - age) + 1;
		}
	}

	DBG(DEV_INOUT, "Leaving function.");
	return (nextchk < 1) ? 1 : nextchk + 1;
}

/* holddown_del
 * Remove a network from holddown.
 */
void holddown_del(hdNetwork * net)
{
	if (net == firstHdNetwork) {
		DBG(DEV_2, "Deleting firstHdNetwork.");
		firstHdNetwork = net->next;
	} else {
		DBG(DEV_2, "This is not the firstHdNetwork.");
	}

	if (net == lastHdNetwork) {
		DBG(DEV_2, "Deleting lastHdNetwork.");
		lastHdNetwork = net->prev;
	} else {
		DBG(DEV_2, "This is not the lastHdNetwork.");
	}

	if (net->prev)
		net->prev->next = net->next;
	if (net->next)
		net->next->prev = net->prev;

	/* Check if we are deleting the final network on holddown */
	if (firstHdNetwork == NULL) {
		pthread_mutex_lock(&mutexHaveHolddown);
		haveHolddown--;
		pthread_mutex_unlock(&mutexHaveHolddown);
		DBG(HOLD_2, "Last holddown prefix is being deleted.");
	}
		
	DBG(HOLD_1, "Removing hash %llu from holddown.", net->hash);
	free(net);
}
